#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: evolution.py
# Author: Jimin Huang <huangjimin@whu.edu.cn>
# Date: 30.10.2017
import numpy as np


DNA_SIZE = 4            # DNA length
POP_SIZE = 100           # population size
CROSS_RATE = 0.8         # mating probability (DNA crossover)
MUTATION_RATE = 0.003    # mutation probability
N_GENERATIONS = 100000
X_BOUNDS = [-10, 10]         # x upper and lower bounds


min_value = 0
min_index = {}
min_pop = []


def F(x):
    results = []
    for element in x:
        result = 1
        for ele in element:
            temp = np.sum(
                [index * np.cos((index+1) * ele + index) for index in xrange(1, 6)]
            )
            result *= temp
        results.append(-result)
    return np.array(results)


def get_fitness(pred):
    pred = pred / 1e4
    return np.exp(pred) / np.sum(np.exp(pred))


def select(pop, fitness):
    idx = np.random.choice(
        np.arange(pop.shape[0]), size=POP_SIZE, replace=True,
        p=fitness/fitness.sum()
    )
    return pop[idx]


def crossover(parent, pop):
    if np.random.rand() < CROSS_RATE:
        while True:
            i_ = np.random.randint(0, POP_SIZE, size=1)
            weight = np.random.random(size=DNA_SIZE)
            temp = np.multiply((1-weight), parent) + np.multiply(weight, pop[i_])[0]
            if np.all(np.logical_and(-10<=temp, temp<=10)):
                parent = temp
                break
    return parent


def mutate(child):
    for point in range(DNA_SIZE):
        if np.random.rand() < MUTATION_RATE:
            child[point] += np.random.random(
                size=1
            ) * 10 - 5 
    return child


pop = np.random.random_sample(
    size=(POP_SIZE, DNA_SIZE)
) * (X_BOUNDS[1] - X_BOUNDS[0])- X_BOUNDS[1]


for _ in range(N_GENERATIONS):
    F_values = F(pop)

    fitness = get_fitness(F_values)
    print("Most fitted DNA: ", pop[np.argmax(fitness), :])
    print(F_values[np.argmax(fitness)])
    if min_value < F_values[np.argmax(fitness)]:
        min_value = F_values[np.argmax(fitness)]
        min_index = set([str(pop[np.argmax(fitness), :])])
        min_pop = [pop[np.argmax(fitness), :]]
    elif min_value == F_values[np.argmax(fitness)]:
        min_index.add(str(pop[np.argmax(fitness), :]))
        if str(pop[np.argmax(fitness), :]) not in min_index:
            min_pop.append(pop[np.argmax(fitness), :])
    pop = select(pop, fitness)
    pop_copy = pop.copy()
    # new_pop = []
    for parent in pop:
        child = crossover(parent, pop_copy)
        child = mutate(child)
        parent[:] = child
    if min_pop:
        pop = np.append(pop, np.array(min_pop), axis=0)

print min_value
print min_index
